package org.iamd.conf;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.iamd.domain.business.ApplicationException;
import org.iamd.security.AccessContext;
import org.iamd.security.ISecurityManager;
import org.iamd.security.SecurityServicesManager;
import org.iamd.security.UserContext;
import org.iamd.util.CommonConfigProps;
import org.iamd.util.CommonMsgKeys;
import org.iamd.util.Messenger;
import org.iamd.util.Messenger.ApplicationLocales;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import sun.misc.BASE64Decoder;

/**
 * Мениджър на конфигурациите управляващ зареждане, експорт, импорт, четене и модификация на
 * системната и потребителска конфигурации, както и инициализиращ конфигурациите на останалите
 * конфигурируеми компоненти в приложението.
 * 
 * @author И. Атанасов
 */
public class ConfigurationManager {

	/**
	 * Енумерация използваща се за зареждане на централната конфигурация.
	 * 
	 * @author И. Атанасов
	 */
	enum MasterConfigTag {

		/** Таг за конфигурационен файл */
		CONFIG_FILE("configFile"),

		/** Таг за конфигурируем компонент */
		CONFIGURABLE_COMPONENT("configurableComponent"),

		/** Таг за фабрика за инициализация на конфигурируем компонент */
		FACTORY_CLASS("factoryClass"),

		/** Централен таг */
		MASTER_CONFIGURATION("masterConfiguration"),

		/** Таг за име */
		NAME("name"),

		/** Таг за параметър на конфигурируем компонент */
		PARAM("param"),

		/** Таг за системни променливи */
		SYSTEM_PROPERTIES("systemProperties"),

		/** Таг за потребителски променливи */
		USER_EDITABLE_PROPERTIES("userEditableProperties"),

		/** Таг за стойност */
		VALUE("value");

		/** Име на таг. */
		private String tagName;

		/**
		 * Инициализация на полето.
		 * 
		 * @param tagName
		 *            име на таг
		 */
		private MasterConfigTag(String tagName) {
			this.tagName = tagName;
		}

	}

	/** Инстанция на мениджъра - singleton pattern. */
	private static ConfigurationManager instance;

	/**
	 * Инстанциране на конфигурационния мениджър. Singleton pattern.
	 * 
	 * @return единствената инстанция на класа
	 * 
	 */
	public static ConfigurationManager getInstance() {
		if (instance == null) {
			instance = new ConfigurationManager();
		}
		return instance;
	}

	/** Уникален идентификатор на логнат в системата администратор. */
	private UUID adminUUID;

	/** Директория където е инсталирано приложението. */
	private File applicationDir;

	/** Конфигурационната директория на приложението. */
	private File confDir;

	/** Централната конфигурация. */
	private boolean isConfigured;

	/** Централната конфигурация. */
	private File masterConfig;

	/** Ключ за криптиране на администраторската парола. */
	private final byte[] sKey = { -85, -58, 0, -106, -4, -84, 28, -90, 20, -2, 41, 77, -109, 94,
			-125, -78 };

	/** Немодифицируема конфигурация. */
	private Configuration systemConfig;

	/** Конфигурация модифицирана от потребителя. */
	private Configuration userConfig;

	/**
	 * Стандартен конструктор.
	 */
	private ConfigurationManager() {
		// private constructor
	}

	/**
	 * Експортва потребителската конфигурация на подаденото място. Методът е синхронизиран, тъй като
	 * инстанцията е само една.
	 * 
	 * @param targetFile
	 *            файл, в който трябва да се запише конфигурацията
	 * 
	 * @throws IOException
	 *             при неуспешен експорт
	 */
	public synchronized void exportUserConf(File targetFile) throws IOException {
		if (this.isConfigured) {
			this.userConfig.exportConfig(targetFile);
		}
	}

	/**
	 * Връща административен потребителски контекст, с цел пълен достъп на ресурси от системни
	 * процеси. За целта {@link CommonConfigProps#ADMIN_USER} и {@link CommonConfigProps#ADMIN_PASS}
	 * трябва да бъдат инициализирани в системната конфигурация. Паролата е криптирана използвайки
	 * ключа {@link ConfigurationManager#sKey}, като след това е ASCII armored. Контекста се
	 * инициализира ако е <code>null</code>, както и всеки път когато е изтекъл.
	 * 
	 * @return намерения контекст
	 * 
	 * @throws ApplicationException
	 *             при възникнало изключение при инициализацията на контекста
	 */
	public synchronized UUID getAdminID() throws ApplicationException {
		if (this.isConfigured) {
			AccessContext.get().pushUniqueId(this.adminUUID);
			ISecurityManager mng = SecurityServicesManager.getInstance().getService(
					ISecurityManager.class);
			UserContext uc = mng.getCurrentSecurityContext();
			AccessContext.get().popUniqueId();
			if (uc == null) {
				try {
					String username = this.systemConfig.getProperty(CommonConfigProps.ADMIN_USER,
							"");
					String password = this.systemConfig.getProperty(CommonConfigProps.ADMIN_PASS,
							"");
					if (!username.equals("") && !password.equals("")) {
						Cipher cipher = Cipher.getInstance("AES");
						cipher.init(Cipher.DECRYPT_MODE,
								new SecretKeySpec(this.sKey, cipher.getAlgorithm()));
						String originalPass = new String(cipher.doFinal(new BASE64Decoder()
								.decodeBuffer(password)));
						this.adminUUID = mng.logon(username, originalPass);
					}
				} catch (Exception e) {
					throw new ApplicationException(CommonMsgKeys.M0001_CREATE_ADM_CTX_FAILED, e);
				}
			}
		}
		return this.adminUUID;
	}

	/**
	 * @return пътя на файловата система до инсталацията на приложението
	 */
	public File getApplicationDir() {
		return this.applicationDir;
	}

	/**
	 * Връща стойност на entry по подаден ключ.
	 * 
	 * @param key
	 *            подадения ключ
	 * 
	 * @return намерената стойност
	 * 
	 * @see ConfigurationManager#getProperty(IConfigProps, String)
	 */
	public String getProperty(IConfigProps key) {
		return getProperty(key, null);
	}

	/**
	 * Връща стойност на entry по подаден ключ. Първо се търси в системната, а след това в
	 * потребителската конфигурация.
	 * 
	 * @param key
	 *            подадения ключ
	 * @param defaultValue
	 *            стойност по подразбиране
	 * 
	 * @return намерената стойност
	 */
	public String getProperty(IConfigProps key, String defaultValue) {
		String result = this.systemConfig.getProperty(key, defaultValue);
		return result != null ? result.equals(defaultValue) ? this.userConfig.getProperty(key,
				defaultValue) : result : this.userConfig.getProperty(key, defaultValue);
	}

	/**
	 * Връща описание на entry по подаден ключ.
	 * 
	 * @param key
	 *            подадения ключ
	 * 
	 * @return намерената стойност
	 * 
	 * @see ConfigurationManager#getPropertyDescription(IConfigProps, String)
	 */
	public String getPropertyDescription(IConfigProps key) {
		return getPropertyDescription(key, null);
	}

	/**
	 * Връща описание на entry по подаден ключ. Първо се търси в системната, а след това в
	 * потребителската конфигурация.
	 * 
	 * @param key
	 *            подадения ключ
	 * @param defaultValue
	 *            стойност по подразбиране
	 * 
	 * @return намерената стойност
	 */
	public String getPropertyDescription(IConfigProps key, String defaultValue) {
		String result = this.systemConfig.getPropertyDescription(key, defaultValue);
		return result != null ? result.equals(defaultValue) ? this.userConfig
				.getPropertyDescription(key, defaultValue) : result : this.userConfig
				.getPropertyDescription(key, defaultValue);
	}

	/**
	 * Импортира файл в наличната потребителска конфигурация. Изпълнява се само ако мениджъра е
	 * конфигуриран. Методът е синхронизиран, тъй като инстанцията е само една.
	 * 
	 * @param sourceFile
	 *            файла за импортиране
	 * 
	 * @throws IOException
	 *             при неуспешна операция
	 */
	public synchronized void importUserConf(File sourceFile) throws IOException {
		if (this.isConfigured) {
			try {
				this.userConfig.importConfig(sourceFile);
			} catch (IOException e) {
				throw e;
			} catch (Exception e) {
				throw new IOException(e);
			}
		}
	}

	/**
	 * Инициализира потребителската, системната и конфигурациите за всеки конфигурируем компонент.
	 * След успешна инициализация флагът <code>isConfigured</code> се вдига. Методът е
	 * синхронизиран, тъй като инстанцията е само една.
	 * 
	 * @param applDir
	 *            директория където е инсталирано приложението
	 * @param configDir
	 *            конфигурационна директория
	 * @param configuration
	 *            централен конфигурационен файл
	 * 
	 * @throws InitializationException
	 *             при неуспешна инициализация
	 */
	public synchronized void initializeConfig(File applDir, File configDir, File configuration)
			throws InitializationException {
		if (!this.isConfigured) {
			try {
				this.applicationDir = applDir;
				this.confDir = configDir;
				this.masterConfig = configuration;
				DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
				DocumentBuilder docBuilder = dbf.newDocumentBuilder();
				Document doc = docBuilder.parse(this.masterConfig);
				Element userPropsElem = (Element) doc.getElementsByTagName(
						MasterConfigTag.USER_EDITABLE_PROPERTIES.tagName).item(0);
				this.userConfig = initConfig(userPropsElem, true);
				Element systemPropsElem = (Element) doc.getElementsByTagName(
						MasterConfigTag.SYSTEM_PROPERTIES.toString()).item(0);
				this.systemConfig = initConfig(systemPropsElem, false);
				NodeList configComponents = doc
						.getElementsByTagName(MasterConfigTag.CONFIGURABLE_COMPONENT.tagName);
				for (int i = 0; i < configComponents.getLength(); i++) {
					Element currElem = (Element) configComponents.item(i);
					String componentName = currElem.getAttribute(MasterConfigTag.NAME.tagName);
					String factoryName = currElem.getAttribute(MasterConfigTag.FACTORY_CLASS
							.toString());
					String configFileName = currElem.getAttribute(MasterConfigTag.CONFIG_FILE
							.toString());
					Map<String, String> params = extractParams(currElem
							.getElementsByTagName(MasterConfigTag.PARAM.tagName));
					ConfigurationFactory confFactory = (ConfigurationFactory) Class.forName(
							factoryName).newInstance();
					try {
						confFactory.initializeComponent(params, new File(this.confDir,
								configFileName));
					} catch (Throwable e) {
						System.err.println(Messenger.formMessage(
								CommonMsgKeys.M0000_COMPONENT_INIT_FAILED,
								new Object[] { componentName }, ApplicationLocales.SYSTEM_LOCALE));
						throw new InitializationException(e);
					}
				}
				this.isConfigured = true;
			} catch (Exception e) {
				throw new InitializationException(e);
			}
		}
	}

	/**
	 * Модифицира и записва потребителската конфигурация. Изпълнява се само ако мениджъра е
	 * конфигуриран. Методът е синхронизиран, тъй като инстанцията е само една.
	 * 
	 * @param props
	 *            променени entry-та
	 * @throws IOException
	 *             при неуспешно съхраняване върху диска
	 */
	public synchronized void modifyUserConf(Properties props) throws IOException {
		if (this.isConfigured) {
			Set<Object> keys = props.keySet();
			for (Object key : keys) {
				String keyStr = (String) key;
				this.userConfig.setProperty(keyStr, props.getProperty(keyStr));
			}
			this.userConfig.saveConfig();
		}
	}

	/**
	 * Модифицира параметър на потребителската конфигурация. Изпълнява се само ако мениджъра е
	 * конфигуриран.
	 * 
	 * @param key
	 *            подаден ключ
	 * @param value
	 *            стойност
	 */
	public void setUserProperty(String key, String value) {
		if (this.isConfigured) {
			this.userConfig.setProperty(key, value);
		}
	}

	/**
	 * По даден списък от DOM елементи, извлича информация за параметрите и техните стойности.
	 * 
	 * @param paramNodes
	 *            списък от DOM елементи
	 * 
	 * @return формирания <code>Map</code> с параметрите и техните стойности
	 */
	private Map<String, String> extractParams(NodeList paramNodes) {
		Map<String, String> result = new HashMap<String, String>();
		for (int i = 0; i < paramNodes.getLength(); i++) {
			Element paramNode = (Element) paramNodes.item(i);
			result.put(paramNode.getAttribute(MasterConfigTag.NAME.tagName),
					paramNode.getAttribute(MasterConfigTag.VALUE.tagName));
		}
		return result;
	}

	/**
	 * Инициализира конфигурация - системна/потребителска.
	 * 
	 * @param propsElem
	 *            дадения DOM елемент.
	 * @param isEditable
	 *            флаг дали конфигурацията трябва да е модифицуруема.
	 * 
	 * @return инстанция на конфигурация
	 * 
	 * @throws InitializationException
	 *             при неуспешна инициализация на конфигурацията
	 */
	private Configuration initConfig(Element propsElem, boolean isEditable)
			throws InitializationException {
		String fileStr = propsElem.getAttribute(MasterConfigTag.CONFIG_FILE.tagName);
		return new Configuration(new File(this.confDir, fileStr), isEditable);
	}

}